package rkw.communication;

import java.net.BindException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;

import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import rkw.database.DatabaseService;
import rkw.database.model.AdministratorToken;
import rkw.database.model.Election;
import rkw.database.model.ElectionVoter;
import rkw.database.model.Member;
import rkw.database.model.MessageReceived;
import rkw.database.model.MessageToSend;
import rkw.database.model.Node;
import rkw.database.model.Voter;
import rkw.election.ElectionService;
import rkw.gui.DlgCommunicationState;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import core.communication.configurationNode.AddConfigurationNode;
import core.communication.configurationNode.ConfigurationNode;
import core.communication.configurationNode.DeleteConfigurationNode;
import core.communication.configurationNode.EditConfigurationNode;
import core.communication.configurationNode.SetParentConfigurationNode;
import core.communication.electionConfiguration.AddEditElectionDetailsElectionConfiguration;
import core.communication.electionConfiguration.AddMemberElectionConfiguration;
import core.communication.electionConfiguration.AddNonResidentVoterElectionConfiguration;
import core.communication.electionConfiguration.AddResidentVoterElectionConfiguration;
import core.communication.electionConfiguration.AddVoterElectionConfiguration;
import core.communication.electionConfiguration.ElectionConfiguration;
import core.communication.electionConfiguration.RemoveElectionElectionConfiguration;
import core.communication.electionConfiguration.RemoveMemberElectionConfiguration;
import core.communication.electionConfiguration.RemoveNonResidentVoterElectionConfiguration;
import core.communication.electionConfiguration.RemoveResidentVoterElectionConfiguration;
import core.communication.electionConfiguration.RemoveVoterOrMemberElectionConfiguration;
import core.communication.message.CNodeState;
import core.communication.message.CommunicationTestMessage;
import core.communication.message.Direction;
import core.communication.message.ElectionCardMessage;
import core.communication.message.ElectionStatusMessage;
import core.communication.message.Message;
import core.communication.message.MessagePriorityQueue;
import core.communication.message.NodeStateEnum;
import core.communication.message.NodeStateMessage;
import core.communication.message.TerminalMessage;
import core.communication.message.TerminalRegistrationMessage;
import core.communication.message.UpdateMemberMessage;
import core.communication.message.UpdateVoterMessage;
import core.communication.message.VerifyNonresidentVoterConfirmationMessage;
import core.communication.message.VerifyNonresidentVoterMessage;
import core.communication.message.VoteMessage;
import core.database.ElectionStatus;
import core.database.NodeType;
import core.exceptions.NoElectionFoundException;
import core.utils.Converter;

@Service
public class CommunicationServiceImpl implements CommunicationService, ApplicationContextAware {
	private static final Logger L = LoggerFactory.getLogger(CommunicationServiceImpl.class);

	private DatabaseService databaseService;
	private Server server;
	private MessageSender messageSender;
	private MessageReceiver messageReceiver;
	private ClientChecker clientChecker;
	private MessagePriorityQueue messagesToSend = new MessagePriorityQueue();
	private MessagePriorityQueue messagesReceived = new MessagePriorityQueue();
	private LinkedList<Client> clientsToCheck = new LinkedList<Client>();
	private HashMap<String, Client> upperClients = new HashMap<String, Client>();
	private HashMap<String, Client> terminals = new HashMap<String, Client>();
	private Node mainNode;
	private ThreadPoolTaskExecutor taskExecutor;
	private ApplicationContext ctx;
	private ElectionService electionService;
	private Shell shell;

	@Inject
	public CommunicationServiceImpl(DatabaseService databaseService, Server server, MessageSender messageSender, MessageReceiver messageReceiver, ClientChecker clientChecker,
			ThreadPoolTaskExecutor taskExecutor) {
		L.debug("Tworzę CommunicationService");

		Assert.notNull(databaseService);
		Assert.notNull(server);
		Assert.notNull(messageSender);
		Assert.notNull(messageReceiver);
		Assert.notNull(clientChecker);
		Assert.notNull(taskExecutor);

		this.databaseService = databaseService;
		this.server = server;
		this.messageSender = messageSender;
		this.messageReceiver = messageReceiver;
		this.clientChecker = clientChecker;
		this.taskExecutor = taskExecutor;

		L.debug("Utworzono CommunicationService");
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.ctx = applicationContext;
	}

	@Override
	public void setUp() throws BindException {
		L.debug("setUp");

		mainNode = databaseService.getMainNode();
		server.setUp(mainNode.getLocatorURI());

		setUpClients();

		setUpMessages();

		taskExecutor.execute(messageSender);
		taskExecutor.execute(messageReceiver);
		taskExecutor.execute(clientChecker);

		electionService = ctx.getBean(ElectionService.class);
	}

	private void setUpMessages() {
		L.debug("setUpMessages");

		List<MessageToSend> messagesTS = databaseService.getMessagesToSend();
		List<MessageReceived> messagesR = databaseService.getMessagesReceived();

		for (MessageToSend messageToSend : messagesTS) {
			Message message = (Message) Converter.xmlToObject(messageToSend.getValue());
			message.setMessageToSendDatabaseId(messageToSend.getId());
			messagesToSend.add(message);
		}

		for (MessageReceived messageReceived : messagesR) {
			Message message = (Message) Converter.xmlToObject(messageReceived.getValue());
			messagesReceived.add(message);
		}
	}

	private void setUpClients() {
		L.debug("setUpClients");

		List<Node> topNodes = databaseService.getNodesForDirection(Direction.UP);
		for (Node node : topNodes)
			upperClients.put(node.getLocatorURI(), new Client(node.getLocatorURI(), node.getNodeType()));
	}

	@Override
	public void shutDown() {
		L.debug("shutDown");

		server.shutdownServer();
		messageSender.stop();
		messageReceiver.stop();
		clientChecker.stop();
	}

	@Override
	public void removeClientToCheck(Client client) {
		L.debug("Usuwam {} do sprawdzenia", client.getLocatorURI());
		synchronized (clientsToCheck) {
			clientsToCheck.remove(client);
		}
	}

	@Override
	public void sendNodeStateMessage(String locatorUri, NodeStateEnum state) {
		L.debug("Wysyłam wiadomość stanu " + locatorUri + " " + state);
		Message message = new NodeStateMessage(locatorUri, new CNodeState(state));
		send(message);
	}

	@Override
	public void send(Message message) {
		L.debug("Wysyłam wiadomość {}", message);

		synchronized (messagesToSend) {
			databaseService.storeMessageToSend(message);
			messagesToSend.add(message);
			messageSender.wakeUp();
		}
	}

	@Override
	public void reAddClientToCheck(Client client) {
		synchronized (clientChecker) {
			clientsToCheck.remove(client);
			clientsToCheck.add(client);
		}
	}

	@Override
	public Client getClientToCheck() {
		Client client = null;
		synchronized (clientsToCheck) {
			if (!clientsToCheck.isEmpty())
				client = clientsToCheck.getFirst();
		}
		return client;
	}

	@Override
	public String getServerURI() {
		return mainNode.getLocatorURI();
	}

	@Override
	public void addReceivedMessage(Message message) {
		L.info("Dodaje otrzymaną wiadomośc {}", message.toString());
		synchronized (messagesReceived) {
			if (!(message instanceof TerminalMessage))
				databaseService.saveReceivedMessage(message);
			messagesReceived.add(message);
			messageReceiver.wakeUp();
		}
	}

	@Override
	public Message getReceivedMessage() {
		Message message = null;
		synchronized (messagesReceived) {
			message = messagesReceived.get();
		}
		return message;
	}

	@Override
	public void removeReceivedMessage(Message message) {
		if (!(message instanceof TerminalMessage))
			databaseService.setMessageAsHandled(message.getMessageToSendDatabaseId());
		messagesReceived.remove(message);
	}

	@Override
	public void handleConfiguration(List<ConfigurationNode> configurationNodes) {
		L.debug("Obsługuje ConfigureationMessage");
		for (ConfigurationNode configurationNode : configurationNodes) {
			L.debug("ConfigurationNode {}" + configurationNode.getClass().getName());

			if (configurationNode instanceof SetParentConfigurationNode) {
				handleSetParentConfigurationNode(configurationNode);
			} else if (configurationNode instanceof AddConfigurationNode) {
				handleAddConfigurationNode(configurationNode);
			} else if (configurationNode instanceof EditConfigurationNode) {
				handleEditConfigurationNode(configurationNode);
			} else if (configurationNode instanceof DeleteConfigurationNode) {
				handleDeleteConfigurationNode(configurationNode);
			}
		}
	}

	private void handleSetParentConfigurationNode(ConfigurationNode configurationNode) {
		SetParentConfigurationNode conf = (SetParentConfigurationNode) configurationNode;
		mainNode.setPrefferedParentURI(conf.getTargetNodeUri());
		mainNode = databaseService.storeNode(mainNode);
	}

	private void handleAddConfigurationNode(ConfigurationNode configurationNode) {
		AddConfigurationNode conf = (AddConfigurationNode) configurationNode;
		Node node = new Node(conf.getChangedNode(), conf.getDirection());
		node = databaseService.storeNode(node);
		if (conf.getDirection() == Direction.UP)
			upperClients.put(node.getLocatorURI(), new Client(node.getLocatorURI(), node.getNodeType()));
	}

	private void handleEditConfigurationNode(ConfigurationNode configurationNode) {
		EditConfigurationNode conf = (EditConfigurationNode) configurationNode;
		Node newNode = new Node(conf.getChangedNode(), conf.getDirection());

		if (conf.getTargetNodeUri().compareTo(newNode.getLocatorURI()) != 0) {
			Client client = null;
			if (upperClients.containsKey(conf.getTargetNodeUri())) {
				client = upperClients.get(conf.getTargetNodeUri());
				client.disconnect();
				upperClients.remove(conf.getTargetNodeUri());
				upperClients.put(newNode.getLocatorURI(), new Client(newNode.getLocatorURI(), newNode.getNodeType()));
			}
		}

		databaseService.updateNode(conf.getTargetNodeUri(), newNode);
	}

	private void handleDeleteConfigurationNode(ConfigurationNode configurationNode) {
		DeleteConfigurationNode conf = (DeleteConfigurationNode) configurationNode;
		databaseService.deleteNode(conf.getTargetNodeUri());

		Client client = upperClients.remove(conf.getTargetNodeUri());
		L.debug("Usuwam klienta " + client.toString());
		removeClientToCheck(client);
		client.disconnect();
	}

	@Override
	public void handleElectionConfiguration(Long electionIdentificationField, List<ElectionConfiguration> electionConfigurations) {
		L.debug("ObsługujeElectionConfigurationMessage dla: " + electionIdentificationField);
		Election election = databaseService.getElection(electionIdentificationField);

		if (election == null)
			election = new Election();

		Map<String, AdministratorToken> tokens = Maps.newHashMap();
		for (AdministratorToken token : election.getAdministratorTokens()) {
			tokens.put(token.getMember().getIdentificationField(), token);
		}

		Map<String, ElectionVoter> voters = Maps.newHashMap();
		for (ElectionVoter voter : election.getVoters()) {
			voters.put(voter.getVoter().getIdentificationField(), voter);
		}

		if (electionConfigurations.size() == 1) {
			if (electionConfigurations.get(0) instanceof AddMemberElectionConfiguration) {
				handleAddMemberTMP(election, electionConfigurations.get(0));
				return;
			}
		}

		for (ElectionConfiguration electionConfiguration : electionConfigurations) {
			L.debug("ElectionConfiguration {}" + electionConfiguration.getClass().getName());

			if (electionConfiguration instanceof AddEditElectionDetailsElectionConfiguration) {
				handleAddEditElectionDetails(election, electionConfiguration);
			} else if (electionConfiguration instanceof AddMemberElectionConfiguration) {
				handleAddMember(election, electionConfiguration);
			} else if (electionConfiguration instanceof AddNonResidentVoterElectionConfiguration) {
				handleAddVoter(election, electionConfiguration, true);
			} else if (electionConfiguration instanceof AddResidentVoterElectionConfiguration) {
				handleAddVoter(election, electionConfiguration, false);
			} else if (electionConfiguration instanceof RemoveElectionElectionConfiguration) {
				handleRemoveElection(election);
				return;
			} else if (electionConfiguration instanceof RemoveMemberElectionConfiguration) {
				handleRemoveMember(tokens, electionConfiguration);
			} else if (electionConfiguration instanceof RemoveNonResidentVoterElectionConfiguration) {
				handleRemoveVoter(voters, electionConfiguration);
			} else if (electionConfiguration instanceof RemoveResidentVoterElectionConfiguration) {
				handleRemoveVoter(voters, electionConfiguration);
			}
		}

		election = databaseService.storeElection(election);
	}

	private void handleAddEditElectionDetails(Election election, ElectionConfiguration electionConfiguration) {
		AddEditElectionDetailsElectionConfiguration conf = (AddEditElectionDetailsElectionConfiguration) electionConfiguration;
		election.setElectionDetails(conf);
	}

	private void handleAddMember(Election election, ElectionConfiguration electionConfiguration) {
		AddMemberElectionConfiguration conf = (AddMemberElectionConfiguration) electionConfiguration;

		Member member = null;

		for (AdministratorToken t : election.getAdministratorTokens()) {
			if (t.getMember().getIdentificationField() == conf.getMember().getIdentificationField()) {
				member = t.getMember();
				break;
			}
		}

		AdministratorToken token = null;
		if (member == null) {
			member = new Member(conf.getMember());
			token = new AdministratorToken(member, election);
		} else {
			for (AdministratorToken t : member.getTokens()) {
				if (t.getElection().getIdentificationField() == election.getIdentificationField()) {
					token = t;
					break;
				}
			}
			if (token == null)
				token = new AdministratorToken(member, election);
		}
		token.setValue(conf.getMember().getAdministratorToken());
	}

	private void handleAddMemberTMP(Election election, ElectionConfiguration electionConfiguration) {
		AddMemberElectionConfiguration conf = (AddMemberElectionConfiguration) electionConfiguration;

		Member member = databaseService.getMemberByIdentificationField(conf.getMember().getIdentificationField());

		if (member != null) {
			AdministratorToken token = databaseService.getAdministratorTokenByMemberAndElection(member, election);
			token.setValue(conf.getMember().getAdministratorToken());
			databaseService.saveAdministratorToken(token);
		}
	}

	private void handleAddVoter(Election election, ElectionConfiguration electionConfiguration, boolean isNonResident) {
		AddVoterElectionConfiguration conf = (AddVoterElectionConfiguration) electionConfiguration;

		Voter voter = databaseService.getVoterByIdentificationField(conf.getVoter().getIdentificationField());

		if (voter == null)
			voter = new Voter(conf.getVoter());

		new ElectionVoter(voter, election, isNonResident);
	}

	private void handleRemoveElection(Election election) {
		List<Member> members = Lists.newLinkedList();
		for (AdministratorToken token : election.getAdministratorTokens())
			members.add(token.getMember());

		List<Voter> voters = Lists.newLinkedList();
		for (ElectionVoter voter : election.getVoters())
			voters.add(voter.getVoter());

		databaseService.deleteElection(election);

		for (Voter voter : voters) {
			voter = databaseService.getVoter(voter.getId());
			if (voter.getElections().size() == 0)
				databaseService.deleteVoter(voter);
		}

		for (Member member : members) {
			member = databaseService.getMember(member.getId());
			if (member.getTokens().isEmpty())
				databaseService.deleteMember(member);
		}
	}

	private void handleRemoveMember(Map<String, AdministratorToken> tokens, ElectionConfiguration electionConfiguration) {
		RemoveVoterOrMemberElectionConfiguration conf = (RemoveVoterOrMemberElectionConfiguration) electionConfiguration;

		AdministratorToken token = tokens.get(conf.getIdentificationField());

		if (token != null)
			token.removeFromElection();
	}

	private void handleRemoveVoter(Map<String, ElectionVoter> voters, ElectionConfiguration electionConfiguration) {
		RemoveVoterOrMemberElectionConfiguration conf = (RemoveVoterOrMemberElectionConfiguration) electionConfiguration;

		ElectionVoter voter = voters.get(conf.getIdentificationField());

		if (voter != null)
			voter.removeFromElection();
	}

	@Override
	public String getPrefferedParentURI() {
		return mainNode.getPrefferedParentURI();
	}

	@Override
	public HashMap<String, Client> getUpperClients() {
		return upperClients;
	}

	@Override
	public HashMap<String, Client> getTerminals() {
		return terminals;
	}

	@Override
	public void resendMessage(Message message) {
		L.debug("Ponownie wysyłam wiadomość {}", message.toString());
		synchronized (messagesToSend) {
			messagesToSend.remove(message);
			messagesToSend.add(message);
			messageSender.wakeUp();
		}
	}

	@Override
	public void removeMessageToSend(Message message) {
		L.debug("Usuwam wiadomośc {}", message.toString());
		synchronized (messagesToSend) {
			databaseService.removeMessageToSend(message.getMessageToSendDatabaseId());
			messagesToSend.remove(message);
		}
	}

	@Override
	public void addClientToCheck(Client client) {
		L.debug("Dodaje {} do sprawdzenia", client.getLocatorURI());
		synchronized (clientsToCheck) {
			clientsToCheck.add(client);
			clientChecker.wakeUp();
		}
	}

	@Override
	public Message getMessageToSend() {
		Message message = null;
		synchronized (messagesToSend) {
			message = messagesToSend.get();
		}

		return message;
	}

	@Override
	public void handleUpdateEntity(Message message) {
		if (message instanceof UpdateVoterMessage) {
			UpdateVoterMessage msg = (UpdateVoterMessage) message;

			Voter voter = databaseService.getVoterByIdentificationField(msg.getVoterIdentificationField());

			if (voter == null)
				voter = new Voter(msg.getVoter());
			else
				voter.updateVoter(msg.getVoter());

			databaseService.storeVoter(voter);

		} else if (message instanceof UpdateMemberMessage) {
			UpdateMemberMessage msg = (UpdateMemberMessage) message;

			Member member = databaseService.getMemberByIdentificationField(msg.getMemberIdentificationField());

			if (member == null)
				member = new Member(msg.getMember());
			else
				member.updateMember(msg.getMember());

			databaseService.storeMember(member);

		}
	}

	@Override
	public void sendVerifyNonResidentVoter(Long electionIdentificationField, String voterIdentificationField, Shell responseShell) {
		this.shell = responseShell;
		VerifyNonresidentVoterMessage msg = new VerifyNonresidentVoterMessage(electionIdentificationField, voterIdentificationField, this.mainNode.getLocatorURI());
		send(msg);
	}

	@Override
	public void handleVoterVerificationConfirmation(VerifyNonresidentVoterConfirmationMessage message) {
		final Voter voter = databaseService.getVoterByIdentificationField(message.getVoterIdentificationField());
		final Boolean verificationResult = message.getVoterVerified();
		if (voter != null) {
			Display.getDefault().asyncExec(new Runnable() {
				@Override
				public void run() {
					if (verificationResult == true) {
						MessageDialog.openInformation(shell, "Information", "Voter " + voter + " can proceed to vote.");
					} else {

						MessageDialog.openInformation(shell, "Information", "Voter " + voter + " has already voted.");
					}
				}
			});
		}

	}

	@Override
	public void handleTerminalRegistrationMessage(TerminalRegistrationMessage message) {
		if (!terminals.containsKey(message.getTerminalLocatorURI()))
			terminals.put(message.getTerminalLocatorURI(), new Client(message.getTerminalLocatorURI(), NodeType.TERMINAL));

		TerminalRegistrationMessage msg;
		msg = new TerminalRegistrationMessage(message.getTerminalLocatorURI(), message.getRegistrationToken(), Direction.DOWN);

		try {
			electionService.getElection(message.getRegistrationToken());

			msg.setRegistrationConfirmation(true);
		} catch (NoElectionFoundException e) {
			msg.setRegistrationConfirmation(false);
		}

		send(msg);
	}

	@Override
	public void handleElectionCardMessage(ElectionCardMessage message) {
		if (electionService.verifyRegistrationToken(message.getRegistrationToken())) {
			ElectionCardMessage msg = new ElectionCardMessage(electionService.getCurrentElection().getCards(), message.getRegistrationToken(), Direction.DOWN, message.getTerminalLocatorURI());
			send(msg);
		}
	}

	@Override
	public void handleVoteMessage(VoteMessage message) {
		if (electionService.verifyRegistrationToken(message.getRegistrationToken())) {
			electionService.handleVote(message.getVoterToken(), message.getVote());
		}
	}

	@Override
	public void sendElectionStatusMessage(Long electionId, ElectionStatus status) {
		ElectionStatusMessage msg = new ElectionStatusMessage(electionId, status, getServerURI());
		send(msg);
	}

	@Override
	public boolean canConnect(String locatorUri) {
		Client c = upperClients.get(locatorUri);
		if (c == null)
			return false;
		if (!c.getIsWorking())
			return false;

		CommunicationTestMessage msg = new CommunicationTestMessage();
		msg.setDestinationURI(locatorUri);
		try {
			c.send(msg);
		} catch (Throwable e) {
			c.setIsWorking(false);
			sendNodeStateMessage(c.getLocatorURI(), NodeStateEnum.OFF);
			addClientToCheck(c);
			return false;
		}
		return true;
	}

	@Override
	@Async
	public void checkCommunication(DlgCommunicationState dlgCommunicationState) {
		List<Client> clients = Lists.newArrayList();
		clients.addAll(upperClients.values());

		Boolean canConnect;

		for (Client client : clients) {
			canConnect = true;
			if (!client.getIsWorking()) {
				canConnect = false;
			} else {
				CommunicationTestMessage msg = new CommunicationTestMessage();
				msg.setDestinationURI(client.getLocatorURI());
				try {
					client.send(msg);
				} catch (Throwable e) {
					client.setIsWorking(false);
					sendNodeStateMessage(client.getLocatorURI(), NodeStateEnum.OFF);
					addClientToCheck(client);
					canConnect = false;
				}
			}

			if (dlgCommunicationState != null)
				dlgCommunicationState.updateNodeState(client.getLocatorURI(), canConnect);
			else
				break;
		}
	}

}
